package article5

//通过一下语句，自动生成代码
//mockgen -source=E:/workspace_go/xzl.com/bbs_test/internal/repository/article/article_repo.go -destination=E:/workspace_go/xzl.com/bbs_test/internal/repository/article/svcmocks/articl
//eRepo.mock.go -package=svcmocks
import (
	"bbs-web/internal/domain"
	"bbs-web/internal/repository/cache"
	"bbs-web/internal/repository/dao"
	"bbs-web/internal/repository/dao/article_dao"
	"bbs-web/pkg/logger"
	"bbs-web/pkg/utils/zifo/slice"
	"context"
	"gorm.io/gorm"
	"time"
)

type ArticleRepository interface {
	List(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error)
	Update(ctx context.Context, art domain.Article) error
	Create(ctx context.Context, art domain.Article) (int64, error)
	Sync(ctx context.Context, art domain.Article) (int64, error)
	SyncStatus(ctx context.Context, author, id int64, status uint8) error //个别修改不用传整个的domain对象
	GetById(ctx context.Context, id int64) (domain.Article, error)
	GetPubById(ctx context.Context, id, uid int64) (domain.Article, error)
	ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]domain.Article, error)
}

type articleRepository struct {
	artDao article_dao.ArticleDAO
	cache  cache.ArticleCache
	l      logger.Logger
	db     *gorm.DB

	u dao.IuserDao
}

func NewArticleRepo(artDao article_dao.ArticleDAO, cache cache.ArticleCache, l logger.Logger, u dao.IuserDao) ArticleRepository {
	return &articleRepository{artDao: artDao, cache: cache, l: l, u: u}
}

func (repo *articleRepository) ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]domain.Article, error) {
	arts, err := repo.artDao.ListPub(ctx, start, offset, limit)
	if err != nil {
		return nil, err
	}
	return slice.Map[dao.ArticleMode, domain.Article](arts, func(idx int, src dao.ArticleMode) domain.Article {
		return repo.toDomain(src)
	}), nil
}

func (repo *articleRepository) GetPubById(ctx context.Context, id, uid int64) (domain.Article, error) {
	//文章详情
	art, err := repo.artDao.GetPubById(ctx, id)
	if err != nil {
		return domain.Article{}, err
	}
	//作者的信息
	user, err := repo.u.FindByID(ctx, uid)
	if err != nil {
		return domain.Article{}, err
	}
	//model组装成domain
	return domain.Article{
		Id:      int64(art.ID),
		Title:   art.Title,
		Content: art.Content,
		Author: domain.Author{
			Id:       int64(user.ID),
			UserName: user.Username,
			NickName: user.Nickname,
			BirthDay: user.Birthday,
			Avatar:   user.Avatar,
		},
		Status:      domain.ArticleStatus(art.Status),
		Summary:     art.Summary,
		ContentType: art.ContentType,
		Cover:       art.Cover,
		Ctime:       art.CreatedAt,
		Utime:       art.UpdatedAt,
	}, nil
}

func (repo *articleRepository) GetById(ctx context.Context, id int64) (domain.Article, error) {
	art, err := repo.artDao.GetById(ctx, id)
	return repo.toDomain(art), err
}

func (repo *articleRepository) Sync(ctx context.Context, art domain.Article) (int64, error) {
	//因为缓存是根据update降序排序的，只要更新、新增数据都会发生变动，所以修改数据库之前要把缓存删掉
	id, err := repo.artDao.Sync(ctx, repo.toEntity(art))
	if err == nil {
		go func() {
			err := repo.cache.DelFirstPage(ctx, art.Author.Id)
			if err != nil {
				repo.l.Error("删除文章列表页缓存失败", logger.Error(err))
			}
			err = repo.cache.SetPub(ctx, art)
			if err != nil {
				repo.l.Error("文章加入缓存失败", logger.Error(err))
			}
		}()
	}
	return id, err

}

func (repo *articleRepository) SyncStatus(ctx context.Context, author, id int64, status uint8) error {
	//考虑是否要吧文章列表页缓存删除
	err := repo.artDao.SyncStatus(ctx, author, id, status)
	if err != nil {
		return err
	}
	return nil
}

func (repo *articleRepository) Update(ctx context.Context, art domain.Article) error {
	//因为缓存是根据update降序排序的，只要更新、新增数据都会发生变动，所以修改数据库之前要把缓存删掉
	go func() {
		err := repo.cache.DelFirstPage(ctx, art.Author.Id)
		if err != nil {
			repo.l.Error("删除文章列表页缓存失败", logger.Error(err))
		}
	}()
	return repo.artDao.UpdateById(ctx, repo.toEntity(art))
}

func (repo *articleRepository) Create(ctx context.Context, art domain.Article) (int64, error) {
	//因为缓存是根据update降序排序的，只要更新、新增数据都会发生变动，所以修改数据库之前要把缓存删掉
	go func() {
		err := repo.cache.DelFirstPage(ctx, art.Author.Id)
		if err != nil {
			repo.l.Error("删除文章列表页缓存失败", logger.Error(err))
		}
	}()
	return repo.artDao.Insert(ctx, dao.ArticleMode{
		AuthorId:    art.Author.Id,
		Title:       art.Title,
		Summary:     art.Summary,
		Content:     art.Content,
		ContentType: art.ContentType,
		Cover:       art.Cover,
		Status:      int(art.Status),
	})
}

func (repo *articleRepository) List(ctx context.Context, uid int64, offset int, limit int) ([]domain.Article, error) {
	//第一页的offset从0开始并且数据量并不大，可以先缓存第一页
	if offset == 0 && limit <= 100 {
		//先查看缓存是否有数据，避免击穿数据库
		page, err := repo.cache.GetFirstPage(ctx, uid)
		if err == nil {
			//可以获取到缓存数据，异步然后预测打开第一条，预加载
			go func() {
				repo.preCache(ctx, page)
			}()

			//处理数组越界问题，就是数据不足pagesize情况
			if limit > len(page) {
				limit = len(page)
			}
			return page[:limit], nil
		}
	}

	//缓存没有数据需要在数据库dao层获取,根据uid查询获取
	data, err := repo.artDao.GetByAuthor(ctx, uid, offset, limit)
	if err != nil {
		return nil, err
	}
	domain_data := slice.Map[dao.ArticleMode, domain.Article](data, func(idx int, src dao.ArticleMode) domain.Article {
		temp := repo.toDomain(src)
		temp.Content = "" //强制不展示列表内容，为了让缓存更加轻量，因为列表不会展示详细信息
		return temp
	})

	//数据获取到的数据之后，把数据写入缓存，回写缓存
	// 回写缓存的时候，考虑是set 还是delete
	/**
	如果创作者不太可能高并发，那么就可以直接set，否则应该del

	*/
	if offset == 0 && limit <= 100 {
		go func() {
			err := repo.cache.SetFirstPage(ctx, uid, domain_data)
			if err != nil {
				repo.l.Error("文章列表回写缓存失败", logger.Error(err))
			}
		}()
	}
	return domain_data, nil
}

// 预加载 预测加载，过期时间短一点
func (repo *articleRepository) preCache(ctx context.Context, data []domain.Article) {
	//考虑文章内容太长就不缓存
	if len(data) > 0 && len(data[0].Content) < 1024*1024 {
		err := repo.cache.Set(ctx, data[0])
		if err != nil {
			repo.l.Error("文章缓存预加载失败", logger.Error(err))
		}
	}
}

func (repo *articleRepository) toDomain(src dao.ArticleMode) domain.Article {
	return domain.Article{
		Id:      int64(src.ID),
		Title:   src.Title,
		Content: src.Content,
		Author: domain.Author{
			Id: src.AuthorId,
		},
		Status:      domain.ArticleStatus(src.Status),
		Summary:     src.Summary,
		ContentType: src.ContentType,
		Cover:       src.Cover,
		Ctime:       src.CreatedAt,
		Utime:       src.UpdatedAt,
	}
}

func (repo *articleRepository) toEntity(art domain.Article) dao.ArticleMode {
	return dao.ArticleMode{
		Model: gorm.Model{
			ID:        uint(art.Id),
			CreatedAt: time.Now(),
			UpdatedAt: time.Now(),
		},
		AuthorId:    art.Author.Id,
		Title:       art.Title,
		Summary:     art.Summary,
		Content:     art.Content,
		ContentType: art.ContentType,
		Cover:       art.Cover,
		Status:      int(art.Status),
	}
}
